iT邦幫忙

2022 iThome 鐵人賽

DAY 22
2
Modern Web

真的好想離開 Vue 3 新手村 feat. CompositionAPI系列 第 22

Day 22: Composition API async setup() + await 的限制

  • 分享至 

  • xImage
  •  

上集回顧

Composition API 元件中的 setup() 函式可以作為非同步函式使用,在內部可以使用關鍵字 await 等待非同步陳述式執行。

一切聽起來很完美,但是!
setup 遇到 await 後就會暫停、返回,返回後,元件的生命週期會繼續執行,這會衍生一些問題和注意事項。

註1:目前直接在 setup() 內使用 await,導致 setup 返回 Promise,Vue 會警告你要加上 <Suspense><Suspense> 會幫你處理接下來提到的問題。
註2:如何在元件非同步取得資料,並渲染到畫面上,請看上一篇

async setup 的問題

await 陳述式之後,setup 函式會暫時返回,繼續跑元件的生命週期,所以在 async setup 中,應該將生命週期 Hooks 和部份 effects 的註冊,寫在 await 之前

await 陳述式之後,部份 Vue API 的調用會失效:

  • 以下函式會無法使用:

    • 生命週期鉤子(Lifecycle Hooks)
    • provideinject
  • 以下函式使用會有點缺陷:

    • watchwatchEffect
    • computed

最好理解的應該是「生命週期鉤子」的部份,因為 setup 返回之後,會繼續跑元件的生命週期,但後面的 Hooks 都還沒註冊完成,當然也沒有效果,對吧?

那為什麼這麼多方法會失效或有點缺點?核心原因是共通的,這些 API 需要綁定元件實例,在非同步期間註冊或初始化,會導致 Vue 找不到對應的元件實例

這也是為什麼 Vue 在生命週期篇章特別說明,一定要讓生命週期被同步註冊,不可以這樣寫:

setTimeout(() => {
  onMounted(() => {
    // 异步注册时当前组件实例已丢失
    // 这将不会正常工作
  })
}, 100)

接下來從註冊生命週期 Hooks 的方式,去了解其中的運作機制,為什麼會找不到對應的元件實例。

運作機制 - 以生命週期鉤子為例

以生命週期 Hooks - onMounted() 為例,開發者會將 callback function 傳入 onMounted,而這個 callback 會在元件掛載完成後被呼叫。

生命週期 Hooks 並不是掛在元件實例下的方法,他會直接被呼叫,也就是說呼叫的時候看不出來他的 context

// 不是這樣直接掛在元件實例下,作為方法呼叫
component.onMounted(`callback function`)

// 是直接呼叫
onMounted(`callback function`)

所以像 onMounted() 要怎麼知道,現在是哪個元件剛掛載完成?

Vue 的作法是宣告一個全域變數,用來儲存剛掛載完成的元件實例,當 Hook 在 setup 函式中被調用時,他們就可以讀取外層宣告的全域變數,來拿到當前元件的實例。

  • 模擬程式碼:Vue 如何紀錄渲染完成的實例
let currentInstance = null

export function mountComponent(component) {
  const instance = createComponent(component)

  // hold the previous instance
  const prev = currentInstance

  // set the instance to global
  currentInstance = instance

  // hooks called inside the `setup()` will have
  // the `currentInstance` as the context
  component.setup() 

  // restore the previous instance
  currentInstance = prev 
}
  • 模擬程式碼:調用 lifecycle hooks
export function onMounted(fn) {
  if (!currentInstance) {
    warn(`"onMounted" can't be called outside of component setup()`)
    return
  }

  // bound listener to the current instance
  currentInstance.onMounted(fn)
}
currentInstance = instance
component.setup() 
currentInstance = prev 

相信熟悉 Javascript 特性的人,已經知道為什麼會使用 await 會影響生命週期鉤子了。

Javascript 是單執行緒的程式語言,原則上會一行一行依序執行程式碼,但如果在 setup() 內使用 await,Javascript 會等待 await 後的非同步陳述式執行完畢,才會接著繼續處理。

currentInstance = instance
component.setup() 
currentInstance = prev

async function setup() {
  console.log(1)
  const users = await getUsersData()
  console.log(2)
  onMounted(() => //執行內容-略//)
}

實際上的執行順序

currentInstance = instance
component.setup()
//console.log(1)
//然後就跑到 event loop 去等待拿回 usersData
currentInstance = prev 

//有可能很久以後,也可能一下之後
//console.log(2)
//onMounted(() =>) 找不到當初的實例

Vue 也不知道非同步什麼時候會執行完畢,在非同步陳述式之後才調用 onMounted(Fn)的話,沒有辦法將元件實例綁定到 context 中。

watchwatchEffectcomputed 非同步註冊的問題

至於 watchwatchEffectcomputed 的缺點是什麼。

就像在 watcher 篇章內提到的。

這三個監聽方法在 setup() 內同步註冊時,會綁定元件實例,這是為了在元件銷毀時,能停止元件內 watcher,避免 memory leak(不會用到的程式持續佔用憶體空間)

如果沒有在 setup() 執行期間同步註冊,他們還是可以正常監聽,但因為沒有連動元件實例,無法在元件被銷毀時自動清除。

結論

基本上,async setup() with await 還是要搭配 <Suspense> 或是其他函式庫提供的 composable 函式來處理。

不過,使用 async setup 的其中一個情境是,單純要將非同步取得的資料,就可以透過昨天提到的--「將非同步函式『變成』同步的響應式資料」來處理,這樣就可以避開 async setup


參考資料


上一篇
Day 21: 來發 API 吧!Async Composition API setup() feat. <Suspense>
下一篇
Day 23: 來發 API 吧!Lifecycle Hooks and Navigation Guards 你要哪一個?
系列文
真的好想離開 Vue 3 新手村 feat. CompositionAPI31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言